package phases

import (
	"fmt"
	"strings"

	"github.com/pkg/errors"
	confv1beta1 "gopkg.openfuyao.cn/bkecommon/cluster/api/v1beta1"
	bkenode "gopkg.openfuyao.cn/bkecommon/cluster/node"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	ctrl "sigs.k8s.io/controller-runtime"

	bkev1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/v1beta1"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/command"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/mergecluster"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/phaseframe"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/pkg/phaseframe/phaseutil"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils/annotation"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils/clusterutil"
	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils/constant"
)

const (
	EnsureDryRunName confv1beta1.BKEClusterPhase = "EnsureDryRun"
)

type EnsureDryRun struct {
	phaseframe.BasePhase
}

func NewEnsureDryRun(ctx *phaseframe.PhaseContext) phaseframe.Phase {
	base := phaseframe.NewBasePhase(ctx, EnsureDryRunName)
	return &EnsureDryRun{base}
}

func (e *EnsureDryRun) Execute() (_ ctrl.Result, err error) {

	if err = e.reconcileDryRun(); err != nil {
		return ctrl.Result{}, err
	}
	return ctrl.Result{}, nil
}

func (e *EnsureDryRun) NeedExecute(old *bkev1beta1.BKECluster, new *bkev1beta1.BKECluster) (needExecute bool) {
	if !new.DeletionTimestamp.IsZero() {
		return false
	}
	if new.Spec.Pause {
		return false
	}
	if new.Spec.DryRun == new.Spec.DryRun {
		return false
	}
	if !new.Spec.DryRun {
		return false
	}
	e.SetStatus(bkev1beta1.PhaseWaiting)
	return true
}

func (e *EnsureDryRun) reconcileDryRun() error {
	ctx, c, bkeCluster, scheme, log := e.Ctx.Untie()

	// only check env and push agent ,do not real change any BKECluster Status
	if bkeCluster.Annotations == nil {
		bkeCluster.Annotations = make(map[string]string)
	}
	annotations := bkeCluster.GetAnnotations()

	if !bkeCluster.Spec.DryRun {
		// remove dryRun annotation
		if _, ok := annotations[annotation.BKEClusterDryRunAnnotationKey]; ok {
			delete(annotations, annotation.BKEClusterDryRunAnnotationKey)
			bkeCluster.SetAnnotations(annotations)
			if err := mergecluster.SyncStatusUntilComplete(c, bkeCluster); err != nil {
				log.Finish(constant.DryRunReason, "failed to update BKECluster Status: %v", err)
				return nil
			}
		}
		return nil
	}

	bkeNodes := phaseutil.GetNeedInitEnvNodes(bkeCluster)

	nodes := bkenode.Nodes{}
	markNodes, ok := annotations[annotation.BKEClusterDryRunAnnotationKey]
	if ok {
		for _, node := range bkeNodes {
			if !strings.Contains(markNodes, node.IP) {
				nodes = append(nodes, node)
			}
		}
	} else {
		nodes = bkeNodes
	}

	if nodes.Length() == 0 {
		log.Info(constant.DryRunReason, "Nodes not changed, no need to dry run")
		return nil
	}

	// add annotation to BKECluster to record dryRun Nodes toavoid reconcile again
	dryRunNodes := ""
	for _, node := range nodes {
		dryRunNodes += node.IP + ","
	}
	annotations[annotation.BKEClusterDryRunAnnotationKey] = dryRunNodes
	bkeCluster.SetAnnotations(annotations)
	if err := mergecluster.SyncStatusUntilComplete(c, bkeCluster); err != nil {
		log.Finish(constant.DryRunReason, "failed to update BKECluster Status: %v", err)
		return nil
	}

	// push agent
	nodeNames := []string{}
	for _, node := range nodes {
		nodeNames = append(nodeNames, phaseutil.NodeInfo(node))
	}
	log.Info(constant.DryRunReason, "Start push BKEAgent to nodes(s), %q", nodeNames)
	localKubeConfigSecret := &corev1.Secret{}
	if err := c.Get(ctx, constant.GetLocalKubeConfigObjectKey(), localKubeConfigSecret); err != nil {
		if apierrors.IsNotFound(err) {
			log.Error(constant.DryRunReason, "Local kubeconfig secret not found")
			return nil
		}
		log.Error(constant.DryRunReason, "Failed to get local kubeconfig secret, err：%v", err)
		return nil
	}
	localKubeConfig := localKubeConfigSecret.Data["config"]
	log.Info(constant.DryRunReason, "Push BKEAgent will take some time, please wait")
	hosts := phaseutil.NodeToRemoteHost(nodes)
	ntpServer := bkeCluster.Spec.ClusterConfig.Cluster.NTPServer
	if failedNodes := phaseutil.PushAgent(hosts, localKubeConfig, ntpServer); len(failedNodes) > 0 {
		errInfo := "Failed to push bkeagent to flowing Nodes"
		log.Error(constant.DryRunReason, "%s: %v", errInfo, failedNodes)
		// todo retry to push bkeagent to failed Nodes ?
		return nil
	}
	log.Info(constant.DryRunReason, "Successfully pushed BKEAgent to all Nodes")
	// todo check bkeagent is ready on all Nodes, if not, retry to push bkeagent to all Nodes?
	// pingBkeAgent
	err, _, failedNodes := e.pingBKEAgent()
	if err != nil {
		log.Error(constant.DryRunReason, "Failed to ping BKEAgent, err: %v", err)
		return nil
	}

	if len(failedNodes) > 0 {
		errInfo := fmt.Sprintf("Failed to ping bkeagent on flow Nodes: %v", failedNodes)
		log.Error(constant.DryRunReason, errInfo)
		return nil
	}

	log.Info(constant.DryRunReason, "BKEAgent is ready on all Nodes")

	//check env
	// check and init node env for k8s
	log.Info(constant.DryRunReason, "Start check and init node env for k8s")

	var extra []string
	if clusterutil.AvailableLoadBalancerEndPoint(bkeCluster.Spec.ControlPlaneEndpoint, bkeCluster.Spec.ClusterConfig.Nodes) {
		extra = append(extra, bkeCluster.Spec.ControlPlaneEndpoint.Host)
	}
	envCommand := command.ENV{
		BaseCommand: command.BaseCommand{
			Ctx:         ctx,
			NameSpace:   bkeCluster.Namespace,
			Client:      c,
			Scheme:      scheme,
			OwnerObj:    bkeCluster,
			ClusterName: bkeCluster.Name,
			Unique:      true,
		},
		Nodes:         nodes,
		BkeConfigName: bkeCluster.Name,
		Extra:         extra,
		DryRun:        bkeCluster.Spec.DryRun,
	}
	if err := envCommand.New(); err != nil {
		errInfo := "Failed to create k8s env init command"
		log.Error(constant.DryRunReason, "%s: %v", errInfo, err)
		return err
	}
	log.Info(constant.DryRunReason, "Waiting for the env check to complete")

	// wait command finish
	err, _, failedNodes = envCommand.Wait()
	if err != nil {
		return errors.Wrap(err, "failed to wait env init command")
	}
	if len(failedNodes) > 0 {
		errInfo := fmt.Sprintf("Failed to check env on flow Nodes: %v", failedNodes)
		log.Finish(constant.DryRunReason, errInfo)
		return nil
	}

	return nil
}

func (e *EnsureDryRun) pingBKEAgent() (error, []string, []string) {
	ctx, c, bkeCluster, scheme, _ := e.Ctx.Untie()
	err, success, failed := phaseutil.PingBKEAgent(ctx, c, scheme, bkeCluster)
	return err, success, failed
}
